/*
 * Copyright 2008-2013, David George, Licensed under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.magneato.mvc;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TreeSet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.queryParser.ParseException;
import org.magneato.dto.MagUser;
import org.magneato.dto.Page;
import org.magneato.dto.SearchResults;
import org.magneato.infra.Generator;
import org.magneato.service.FileUploader;
import org.magneato.service.Repository;
import org.magneato.service.TemplateManager;
import org.magneato.service.TransactionLogger;
import org.magneato.utils.Authorize;
import org.magneato.utils.Template;
import org.magneato.utils.TemplateGroup;
import org.magneato.utils.TemplateGroupNotFoundException;
import org.magneato.utils.parsers.WikiParser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

/**
 * Main page operations entry point
 * 
 * @author David B George
 */
@Controller
public class PageController {
	@Autowired
	private Repository repository;
	@Autowired
	private TemplateManager templateMgr;
	@Autowired
	private FileUploader fileUploader;
	@Autowired
	private WikiParser wikiParser;

	@Autowired
	private TransactionLogger transLogger;

	@Value("${umask}")
	private long umask;
	@Value("${virtualHost}")
	private String virtualHost;


	private Ehcache cache;

	private final static String STARTTAG = "<";

	private final Log _logger = LogFactory.getLog(PageController.class);

	/**
	 * Adds a unique sequence number to the parent page name to create a new
	 * page name. Useful for automagically generating children based on parent
	 * page name
	 * 
	 * @param generator
	 *            - Name of sequence generator, follows
	 *            packagename.NAMEGenerator pattern
	 * @param parent
	 *            - parent page name
	 * @return ModelView with page creation information
	 * 
	 * @throws Exception
	 */
	@RequestMapping(value = "/createPageWizard.do", method = RequestMethod.GET, params = {
			"generator", "parent" })
	public ModelAndView createPageWizardSeq(String generator, String parent)
			throws Exception {
		if (parent == null || parent.isEmpty()) {
			throw new Exception("Parent is null or empty");
		}

		generator = "org.magneato.infra." + generator + "Generator";
		Page page = repository.getParentForCreate(parent);

		try {
			Class<Generator> generatorClass = (Class<Generator>) Class
					.forName(generator);
			Generator g = (Generator) generatorClass.newInstance();
			return createPageWizard(g.getId(page), page, null, false);
		} catch (Exception e) {
			throw new Exception("Error initializing sequence generator");
		}

	}

	/**
	 * Gets called after the user tries to access a page that doesn't exist or
	 * if we are creating a new child page (page name is null)
	 * 
	 * A user must select the page template. If the page referrer is given the
	 * template choice will be based on the referrer's child templates.
	 * 
	 * Security: repository.getParentForCreate needs create permission on the
	 * parent or umask must allow "Other" creation if parent is empty/null
	 * 
	 * @param String
	 *            pageName - name of page to create, this can be empty, in this
	 *            case the parent name must be set and must be a valid page in
	 *            the repository
	 * @param String
	 *            parent, can be empty if pageName is set.
	 * @return Select template view
	 * @throws TemplateGroupNotFoundException
	 */
	@RequestMapping(value = "/createPageWizard.do", method = RequestMethod.GET, params = {
			"name", "parent" })
	public ModelAndView createPageWizardName(String name, String parent)
			throws Exception {

		Page page = repository.getParentForCreate(parent);
		return createPageWizard(name, page, null, false);
	}

	/**
	 * Used by totally anonymous pages with no parent relation
	 * 
	 * @param namespace
	 *            Namespace
	 * @param template
	 *            Template to use for new page
	 * @return Create Wizard Model and View
	 * @throws Exception
	 *             If we don't have permissions
	 */
	@RequestMapping(value = "/createPageWizard.do", method = RequestMethod.GET, params = {
			"namespace", "template" })
	public ModelAndView createPageWizardNameSpace(String namespace,
			String template, boolean autoId) throws Exception {

		// we need Other Create permission in order to create orphans
		repository.getParentForCreate("");
		ModelAndView view = new ModelAndView("sys/selectTemplate");
		Collection<Template> list = new ArrayList<Template>();

		TemplateGroup tg = templateMgr.getTemplateGroup(namespace);
		Template t = tg.getTemplate(template);
		if (t == null) {
			throw new Exception("Template not found " + template);
		}
		list.add(t);
		view.addObject("templates", list);
		if (autoId) {
			long id = System.currentTimeMillis() / (1000 * 60);
			view.addObject("id", id);
		}
		view.addObject("nameSpace", namespace);
		return view;
	}

	private ModelAndView createPageWizard(String name, Page page,
			String nameSpace, boolean autoId) throws Exception {
		String templateName = null;
		String parent;

		if (page != null) {
			// find out the name of the edit template
			templateName = page.getEditTemplate();
			parent = page.getName();
		} else {
			// parent doesn't exist in repo, name is of no interest to us
			parent = "";
		}

		// get namespace of page we want to create
		if (nameSpace == null) {
			if (name == null || name.isEmpty()) {
				if (parent.isEmpty()) {
					throw new Exception(
							"Error: No page name or parent page supplied");
				}

				// TODO namespace should be part of template namespace, put in
				// check
				nameSpace = Page.getNameSpace(parent);
			} else {
				nameSpace = Page.getNameSpace(name);
			}
		}

		// if the template name is null the list will be all templates
		Collection<Template> templateList = templateMgr.getTemplateGroup(
				nameSpace).getChildTemplates(templateName);

		ModelAndView view = new ModelAndView("sys/selectTemplate");
		view.addObject("name",
				URLEncoder.encode(Page.getShortName(name), "UTF-8"));
		view.addObject("templates", templateList);
		view.addObject("parent", parent);
		view.addObject("nameSpace", nameSpace);
		if (autoId) {
			long id = System.currentTimeMillis() / (1000 * 60);
			view.addObject("autoId", id);
		}

		return view;
	}

	/**
	 * Create a new page - this is typically called from the createPageWizard
	 * view. A user would require create permissions on the page.
	 * 
	 * Pages will only be created if they don't already exist.
	 * 
	 * to create a page we need to know the user, obtain his primary group, we
	 * need to know the referring page for permissions
	 * 
	 * Needs either Admin permission or Create on referring page or Create all
	 * umask
	 * 
	 * @param request
	 * @param response
	 * @return
	 * @throws IOException
	 * @throws CorruptIndexException
	 */
	@RequestMapping(value = "/createPage.do", method = RequestMethod.POST, produces = "text/html; charset=utf-8")
	public ModelAndView createPage(HttpServletRequest request, String title,
			String name, String editTemplate, String parent, String nameSpace)
			throws CorruptIndexException, IOException {

		// validation
		// pagename can't be null, neither can template name
		// must be authorized
		// can't use asserts as they can be turned off
		// assert pageName is of correct format
		// assert (!pageName.isEmpty()); // TODO

		// System.out.println("client locale "
		// + request.getLocale().getDisplayLanguage());

		if (Page.ROOT.equals(nameSpace)) {
			name = "/" + name;
		} else {
			name = "/" + nameSpace + "/" + name;
		}
		// Allow Parent Page to be lazily loaded if we want to access data
		Page parentPage = new Page();
		parentPage.setRepository(repository);
		parentPage.setWikiParser(wikiParser);
		parentPage.setName(parent);

		ModelAndView view = new ModelAndView(Page.getNameSpace(name) + "/"
				+ TemplateGroup.PREQUEL + editTemplate, "name", name);
		view.addObject("template", editTemplate);
		view.addObject("parent", parentPage);
		view.addObject("title", title);
		view.addObject("origin", getOrigin(request));
		return view;
	}

	/**
	 * Save Page
	 * 
	 * The page contents are sent in a stream, meta information in a command
	 * object. We expect to find contents contained in a
	 * <contents>...</contents> xml block
	 * 
	 * @param request
	 * @param response
	 * @param name
	 *            - name of page to be saved (and created if necessary). Cannot
	 *            be null or empty.
	 * @param parent
	 *            - Parent page for access rights. Can be empty.
	 * @param templateName
	 *            - Edit template name - cannot be NULL if creating page
	 * @param title
	 *            - Page title, Cannot be NULL if creating page
	 * @return
	 * @throws Exception
	 */
	@RequestMapping(value = "/save.do", method = RequestMethod.POST)
	public ModelAndView save(HttpServletRequest request,
			HttpServletResponse response, String name, String title,
			String templateName, String parent) throws Exception {
		if (name == null || name.isEmpty()) {
			throw new Exception("Error: page name is empty or null");
		}

		// get page for save
		Page page = repository.get(name);

		if (page == null) {
			// page does not exist yet, create it, we must have create
			// permission on the parent
			page = new Page();
			page.setTitle(title);
			// strips out slashes
			//name = WikiParser.cleanURLText(name);
			page.setName(name);

			Page parentPage = repository.get(parent);
			if (parentPage != null) {
				page.setPerms(parentPage.getPerms());
				page.setParent(parentPage.getUuid());
			} else {
				// No valid parent, we are an orphan, in this case we need
				// create permissions on the default home page if it exists
				page.setPerms(umask);
				parent = ""; // umask must allow Other Create Permission
			}

			page.setIpAddr(request.getRemoteAddr());
			MagUser userDetails = Authorize.getUserDetails();
			page.setAuthor(userDetails.getScreenname());
			page.setGroup(Authorize.getPrincipalGroup());

			page.setCreateDate(System.currentTimeMillis());

			// page.setRelations(relations)

			Template t = templateMgr.getTemplateGroup(page.getNameSpace())
					.getTemplate(templateName);
			page.setViewTemplate(t.getDisplay());
			page.setEditTemplate(templateName);
			page.setStatus(Page.Status.DRAFT);
			page.setContent(getContentsFromRequest(request, page));
			repository.createDocument(parent, page);
			transLogger.logUpdate(page, "Page created");
		} else {
			transLogger.logUpdate(page, "Page updated");
			page.setContent(getContentsFromRequest(request, page));
			repository.addDocument(page);
		}

		String url = request.getContextPath() + name;

		if (cache != null) {
			// listCacheKeys();
			// we will want to evict all related pages plus home page
			cache.remove("GET" + url + ".htmnull");
			// listCacheKeys();
		}

		if (virtualHost.isEmpty()) {
			// generate redirect url
			url = request.getRequestURL().toString();
			url = url.substring(0, url.lastIndexOf('/')) + name + ".htm";
			return new ModelAndView("redirect:" + name + ".htm");
		} else {
			/*
			 * redirect via this view to avoid an Orbeon handling error with
			 * sending redirect straight to Orbeon when using virtual hosts
			 */
			return new ModelAndView("sys/redirect", "url", virtualHost + name
					+ ".htm");
		}
	}

	private void listCacheKeys() {
		List cacheKeys = cache.getKeysWithExpiryCheck();
		Iterator i = cacheKeys.iterator();
		while (i.hasNext()) {
			String key = (String) i.next();
			_logger.debug("key<" + key + ">");
			System.out.println("1.key<" + key + ">");
		}
	}

	private String getContentsFromRequest(HttpServletRequest request, Page page)
			throws IOException, URISyntaxException {
		StringBuilder stringBuilder = new StringBuilder();
		BufferedReader bufferedReader = null;
		// TODO: what about versioning? store as page-0 etc or use systime?
		// need version depth, this should be handled in the repository service
		// layer
		try {
			bufferedReader = new BufferedReader(new InputStreamReader(
					request.getInputStream()));

			char[] charBuffer = new char[4096];
			int bytesRead;
			while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
				stringBuilder.append(charBuffer, 0, bytesRead);
			}// while
		} catch (IOException ex) {
			_logger.error(ex.toString());
			throw ex;
		} finally {
			if (bufferedReader != null) {
				try {
					bufferedReader.close();
				} catch (IOException ex) {
					throw ex;
				}
			}
		}

		// clip out the content part
		int end = 0;
		String oldContents = stringBuilder.toString();
		StringBuilder contents = new StringBuilder();
		for (int i = 0; i != -1;) {
			int last = i;
			i = oldContents.indexOf(">file:", i);

			// only copy if file has just been uploaded
			if (i > -1) {
				// we need to know name here
				end = oldContents.indexOf("</", i);
				String filePath = oldContents.substring(i + 1, end);
				_logger.debug("filePath |" + filePath + "|");

				// try and find a filename for xform uploads
				int f = oldContents.lastIndexOf("filename=\"", i);
				int e = oldContents.indexOf("\"", f + 10);
				String fileName = oldContents.substring(f + 10, e);
				_logger.debug("filename |" + fileName + "|");

				// remove query string

				int qs = filePath.indexOf('?');

				if (qs > 0) {
					filePath = filePath.substring(0, qs);
				}
				URL u = new URL(filePath);

				String url = fileUploader.upload(new File(u.toURI()), fileName,
						page);
				_logger.debug(url);

				// now copy the whole shebang over to our new contents
				contents.append(oldContents.substring(last, i + 1));
				contents.append(url);
				i = end; // skip
			}
		}

		contents.append(stringBuilder.substring(end));
		_logger.debug("new contents " + contents);

		// search for closing ?>
		int start = contents.indexOf(STARTTAG, 1);
		end = contents.lastIndexOf(">");
		return contents.substring(start, end + 1);
	}

	/**
	 * @param name
	 * @return
	 */
	@RequestMapping(value = "/edit.do", method = RequestMethod.GET)
	public ModelAndView edit(HttpServletRequest request,
			HttpServletResponse response, String name) {
		Page page = new Page();
		page.setName(name);

		// checks we have write permission
		page = repository.getForEdit(page);

		request.getSession().setAttribute("redirect",
				"/edit.do?pageName=" + name);

		response.setContentType("text/html; charset=utf-8");

		ModelAndView view = new ModelAndView(page.getNameSpace() + "/"
				+ TemplateGroup.PREQUEL + page.getEditTemplate(), "name", name);
		view.addObject("origin", getOrigin(request));
		request.setAttribute("my-user-data", page.getContent());

		return view;
	}

	/**
	 * 
	 * @param request
	 * @param locale
	 * @param template
	 *            Search within this group of templates
	 * @param namespace
	 *            Search within this namespace
	 * @return Navigation view
	 * @throws ResourceNotFoundException
	 *             - If namespace is not legal format
	 */
	@RequestMapping(value = "/navigate.do", method = RequestMethod.GET)
	public ModelAndView navigate(HttpServletRequest request, Locale locale,
			String template, String namespace,
			@RequestParam(value = "style", defaultValue = "1") String style,
			@RequestParam(value = "start", defaultValue = "0") Integer start,
			@RequestParam(value = "count", defaultValue = "10") Integer count)
			throws ResourceNotFoundException {
		ModelAndView view = new ModelAndView("sys/navigate");

		SearchResults results = repository.facetNavigation(template, namespace,
				request.getParameterMap(), start, count);

		view.addObject("facets", results);
		view.addObject("template", template);
		view.addObject("namespace", namespace);
		view.addObject("style", style);
		return view;
	}

	/**
	 * Returns all the possible values for a facet based on various filters
	 * 
	 * @param facetName
	 *            Name of facet to get
	 * @param template
	 *            Template that facet belongs to
	 * @param searchTerm
	 *            Filter on facet values (e.g. all facet values starting with
	 *            F*)
	 * @param facets
	 *            List of other facets to filter by... this doesn't work as we
	 *            only have the facet value, we need make=ford, model=escort
	 */
	@RequestMapping(value = "/getFacetValues.do", method = RequestMethod.POST)
	public ModelAndView navigate(HttpServletRequest request,
			HttpServletResponse response, String facetName, String template,
			String searchTerm, String[] facets) {

		response.setContentType("application/xml");

		// for (String xfacet : facets) {
		// System.out.println("facets: " + xfacet);
		// }

		TreeSet<String> values = null;
		try {
			values = repository.getFacetValues(facetName, template, searchTerm);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (ParseException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		values.add(searchTerm);

		return new ModelAndView("sys/facets", "values", values);
	}

	/**
	 * Perform repository search, needs GET method for paging
	 * 
	 * @param keyWords
	 *            Query terms
	 * @param start
	 *            Start position in repository
	 * @param end
	 *            Number of results to fetch
	 * @return SearchResults object
	 * @throws ResourceNotFoundException
	 */
	@RequestMapping(value = "/search.do", method = { RequestMethod.GET,
			RequestMethod.POST })
	public ModelAndView search(String keyWords, int start, int end)
			throws ResourceNotFoundException {

		ModelAndView view = new ModelAndView("sys/search");

		/*
		 * int totalHits = 135; SearchResults results = new SearchResults(start,
		 * end, totalHits, keyWords); String[] hits = getResults(start, end,
		 * totalHits); for (int i = 0; i < hits.length; i++) { Result result =
		 * new Result(hits[i]); result.setTitle(hits[i]);
		 * result.setDate("date"); results.addResult(result); }
		 */

		SearchResults results = repository.search(keyWords, start, end);
		view.addObject("search", results);
		return view;
	}

	private String[] getResults(int start, int count, int totalHits) {
		String[] results = new String[count];
		int first = start * count;

		for (int i = first; i < first + count && i < totalHits; i++) {
			results[i - first] = "result " + i;
		}

		return results;
	}

	/**
	 * Used by Orbeon to determine where to post to if using Virtual Hosting
	 * 
	 * @param request
	 * @return
	 */
	private String getOrigin(HttpServletRequest request) {
		String origin;

		if (virtualHost.isEmpty()) {
			origin = request.getContextPath();
		} else {
			origin = request.getScheme() + "://" + request.getServerName()
					+ ":" + request.getServerPort() + "/"
					+ request.getContextPath();
		}

		return origin;
	}

	@Value("${cacheName}")
	public void setCacheName(String cacheName) {
		CacheManager manager = CacheManager.getInstance();
		cache = manager.getEhcache(cacheName);
	}

	@ResponseStatus(value = HttpStatus.FORBIDDEN)
	public class ResourceNotFoundException extends RuntimeException {
		public ResourceNotFoundException(String message) {
			super(message);
		}

	}

	/*
	 * Dummy controller to return access Denied view.
	 */
	@RequestMapping(value = "/accessDenied.do", method = RequestMethod.GET)
	public ModelAndView accessDenied(String pageName) {
		return new ModelAndView("sys/accessDenied");
	}

	/*
	 * Redirect to a controller, if we are in the Orbeon context we don't want
	 * Orbeon processing the view
	 */
	@ExceptionHandler(AccessDeniedException.class)
	public ModelAndView myExceptionHandler(AccessDeniedException exception) {
		exception.printStackTrace();
		return new ModelAndView("redirect:/accessDenied.do");
	}
}
